﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Text;
using SSG.Properties;
using SSG.Tools;
using SSG.View;

namespace SSG.Model
{
    /// <summary>
    /// Doing the transformation work.
    /// </summary>
    internal class Generator : INotifyPropertyChanged
    {
        public const string PlaceholderAirportIcao = "<icao>";
        public const string PlaceholderType = "<type>";
        public const string PlaceholderAirac = "<airac>";
        public const string PlaceholderFiletype = "<ese|sct>";
        public const string PlaceholderGroup = "<group>";
        public const string PlaceholderRunway = "<runway>";
        public const string PlaceholderTrackName = "<trackname>";
        public const string PlaceholderTrackSmartName = "<tracksmartname>";
        public const string PlaceholderAppendix = "<appendix>";
        public static readonly List<string> Placeholders = new List<string>(new[] {PlaceholderAirportIcao, PlaceholderType, PlaceholderAirac, PlaceholderFiletype, PlaceholderGroup, PlaceholderRunway, PlaceholderTrackName, PlaceholderTrackSmartName, PlaceholderAppendix});
        public const string SctEseOutputComment = ";-";
        public const string SctEseOutputCommentCr = ";-\n";
        public const string SctEseOutputCommentSpace = ";- ";
        public const string SctEseOutputCommentExtended = ";- //";
        public const string SctEseOutputCommentExtendedSpace = ";- // ";
        public const string SctEseOutputCommentExtendedCr = ";- //\n";
        public const string SctEseOutputHeader = ";+";
        public const string SctEseOutputHeaderCr = ";+\n";
        public const string SctEseOutputHeaderSpace = ";+ ";
        public const string SctEseCommentSeperator = ", ";
        private readonly GeneratorSettings _settings = null;
        private readonly ObservableCollection<string> _sidStarAirports = new ObservableCollection<string>();

        /// <summary>
        /// Property has been changed
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// The SID / STAR Airports.
        /// </summary>
        public ObservableCollection<string> SidStarAirports { get { return this._sidStarAirports; } }

        /// <summary>
        /// Generate lines with runway name?
        /// </summary>
        public List<Setting> RunwayDesignatorInclusionOptions { get { return Setting.RunwayDesignatorInclusionOptions; } }

        /// <summary>
        /// Order options
        /// </summary>
        public List<Setting> OrderOptions { get { return Setting.OrderBy; } }

        /// <summary>
        /// SID order
        /// </summary>
        public Setting SidOrder { get { return this.Settings.SctSidOrder; } set { this.Settings.SctSidOrder = value; } }

        /// <summary>
        /// STAR order
        /// </summary>
        public Setting StarOrder { get { return this.Settings.SctStarOrder; } set { this.Settings.SctStarOrder = value; } }

        /// <summary>
        /// Transition order
        /// </summary>
        public Setting TransitionOrder { get { return this.Settings.SctTransitionOrder; } set { this.Settings.SctTransitionOrder = value; } }

        /// <summary>
        /// Settings.
        /// </summary>
        public GeneratorSettings Settings { get { return this._settings; } }

        /// <summary>
        /// New generator.
        /// </summary>
        /// <param name="settings"></param>
        public Generator(GeneratorSettings settings)
        {
            this._settings = settings;
        }

        /// <summary>
        /// Write a status message.
        /// </summary>
        /// <param name="msg"></param>
        /// <param name="severity"></param>
        private void WriteStatusMsg(string msg, ConsoleAndStatus.Severity severity)
        {
            if (String.IsNullOrWhiteSpace(msg)) return;
            if (this._settings == null || this._settings.ConsoleAndStatus == null) return;
            this._settings.ConsoleAndStatus.WriteStatusMsg(msg, severity);
        }

        /// <summary>
        /// Console message.
        /// </summary>
        /// <param name="msg"></param>
        /// <param name="severity"></param>
        private void WriteConsoleMsg(string msg, ConsoleAndStatus.Severity severity)
        {
            if (String.IsNullOrWhiteSpace(msg)) return;
            if (this._settings == null || this._settings.ConsoleAndStatus == null) return;
            this._settings.ConsoleAndStatus.WriteConsoleMsg(msg, severity);
        }

        /// <summary>
        /// Read the SID / STAR airports and the navaids.
        /// </summary>
        /// <param name="withMessages"></param>
        /// <returns></returns>
        public bool ReadAirportNavaidsAndRunways(bool withMessages = true)
        {
            bool ok = this.ReadSidStarAirports(withMessages);
            ok = ok && this.ReadNavaidsRunwaysAirports(withMessages);
            return ok;
        }

        /// <summary>
        /// Read the navaids, runways, airports
        /// </summary>
        /// <param name="withMessages"></param>
        /// <returns></returns>
        private bool ReadNavaidsRunwaysAirports(bool withMessages)
        {
            // checks
            if (!this.Settings.ExistsNavaidsDirectory) {
                if (withMessages) this.WriteStatusMsg(Resources.ReadNavaidsNoSourceDirectory, ConsoleAndStatus.Severity.Error);
                return false;
            }

            if (!this.Settings.ExistsRunwaysFile) {
                if (withMessages) this.WriteStatusMsg(String.Format(Resources.ReadRunwaysNoFile, this.Settings.RunwaysPmdgFile), ConsoleAndStatus.Severity.Error);
                return false;
            }

            if (!this.Settings.ExistsNavaidsFile) {
                if (withMessages) this.WriteStatusMsg(String.Format(Resources.ReadNavaidsNoFile, this.Settings.NavaidsPmdgFile), ConsoleAndStatus.Severity.Error);
                return false;
            }

            if (!this.Settings.ExistsAirportsFile) {
                if (withMessages) this.WriteStatusMsg(String.Format(Resources.ReadAirportsNoFile, this.Settings.AirportsPmdgFile), ConsoleAndStatus.Severity.Error);
                return false;
            }

            try {
                // Runways
                List<Runway> runways = Runway.ReadNavigraphPmdgRunways(this.Settings.RunwaysPmdgFile);
                if (withMessages) {
                    if (runways.NullSafeIsEmpty())
                        this.WriteStatusMsg(String.Format(Resources.ReadRunwaysEmpty, this.Settings.RunwaysPmdgFile), ConsoleAndStatus.Severity.Error);
                    else
                        this.WriteStatusMsg(String.Format(Resources.ReadRunways, runways.Count), ConsoleAndStatus.Severity.Info);
                }
                if (runways.NullSafeIsEmpty()) return false;

                // Airports
                List<Airport> airports = Airport.ReadNavigraphPmdgAirports(this.Settings.AirportsPmdgFile); // pass runways to link with airports in background
                if (withMessages) {
                    if (airports.NullSafeIsEmpty())
                        this.WriteStatusMsg(String.Format(Resources.ReadAirportsEmpty, this.Settings.AirportsPmdgFile), ConsoleAndStatus.Severity.Error);
                    else
                        this.WriteStatusMsg(String.Format(Resources.ReadAirports, airports.Count), ConsoleAndStatus.Severity.Info);
                }
                if (airports.NullSafeIsEmpty()) return false;

                // Navaids
                List<Navaid> navaids = Navaid.ReadNavigraphPmdgNavaids(this.Settings.NavaidsPmdgFile);
                if (withMessages) {
                    if (navaids.NullSafeIsEmpty())
                        this.WriteStatusMsg(String.Format(Resources.ReadNavaidsEmpty, this.Settings.NavaidsPmdgFile), ConsoleAndStatus.Severity.Error);
                    else
                        this.WriteStatusMsg(String.Format(Resources.ReadNavaids, navaids.Count), ConsoleAndStatus.Severity.Info);
                }
                return !navaids.NullSafeIsEmpty();
            }
            catch (Exception ex) {
                this.WriteStatusMsg(String.Format(Resources.ReadNavaidsRunwaysAirportsException, ex.Message), ConsoleAndStatus.Severity.Error);
                this.WriteConsoleMsg(ex.StackTrace, ConsoleAndStatus.Severity.Error);
                return false;
            }
        }

        /// <summary>
        /// Read the SID / STAR airports.
        /// </summary>
        /// <param name="withMessages"></param>
        /// <returns></returns>
        private bool ReadSidStarAirports(bool withMessages)
        {
            if (this._settings == null) throw new InvalidOperationException("Missing settings");
            bool read = false;
            this._sidStarAirports.Clear();
            if (!this._settings.ExistsSidStarDirectory) {
                if (withMessages) this.WriteStatusMsg(Resources.ReadAirportsNoSourceDirectory, ConsoleAndStatus.Severity.Error);
            }
            else {
                DirectoryInfo di = new DirectoryInfo(this._settings.SidStarDirectory);
                FileInfo[] sidStarFiles = di.GetFiles("*.txt");
                // ReSharper disable LoopCanBeConvertedToQuery
                foreach (FileInfo fi in sidStarFiles) {
                    string airportIcao = Path.GetFileNameWithoutExtension(fi.Name);
                    if (String.IsNullOrEmpty(airportIcao)) continue;
                    this._sidStarAirports.Add(airportIcao);
                }
                // ReSharper restore LoopCanBeConvertedToQuery
                read = true;
                if (withMessages) this.WriteStatusMsg(String.Format(Resources.ReadAirportsWithSidStars, this._sidStarAirports.Count), ConsoleAndStatus.Severity.Info);
            }
            this.OnPropertyChanged(ObjectTools.GetPropertyName(() => this.SidStarAirports));
            return read;
        }

        /// <summary>
        /// Generate files for given airports (as ICAO).
        /// </summary>
        /// <param name="airportIcaos"></param>
        /// <param name="fileNamePattern"></param>
        /// <param name="withMessages"></param>
        /// <returns></returns>
        public bool GenerateFiles(List<string> airportIcaos, string fileNamePattern, bool withMessages = true)
        {
            // entry checks
            if (airportIcaos.NullSafeIsEmpty()) {
                if (withMessages) this.WriteStatusMsg(Resources.GenerateNoAirportsSelected, ConsoleAndStatus.Severity.Warning);
                return false;
            }
            if (!Directory.Exists(this.Settings.DestinationDirectory)) {
                if (withMessages) this.WriteStatusMsg(String.Format(Resources.GenerateNoDestinationDirectory, this.Settings.DestinationDirectory), ConsoleAndStatus.Severity.Error);
                return false;
            }

            foreach (string airportIcao in airportIcaos) {
                String file = this.Settings.SidStarDirectory + airportIcao.ToUpperInvariant() + ".txt";
                String logFile = this.Settings.DestinationDirectory + airportIcao.ToUpperInvariant() + ".log.txt";
                String logMsg = null;
                try {
                    Airport airport = Airport.ReadNavigraphPmdgSidStarFile(file);
                    if (airport == null) {
                        logMsg = String.Format(Resources.GenerateCannotReadAirport, file);
                        if (withMessages) this.WriteStatusMsg(logMsg, ConsoleAndStatus.Severity.Error);
                    }
                    else {
                        // write files
                        this.WriteSctEseFiles(airport, fileNamePattern);
                        this.WriteKmlFile(airport, fileNamePattern);
                    }
                }
                catch (Exception e) {
                    logMsg = String.Format(Resources.GenerateAirportError, airportIcao, e.Message);
                    if (withMessages) {
                        this.WriteStatusMsg(logMsg, ConsoleAndStatus.Severity.Error);
                        this.WriteConsoleMsg(e.StackTrace, ConsoleAndStatus.Severity.Error);
                    }
                }

                // log file
                if (!String.IsNullOrEmpty(logMsg)) this.WriteContentToTexfile(logFile, logMsg);
            }
            return true;
        }

        /// <summary>
        /// Generate the output files
        /// </summary>
        /// <param name="airport"></param>
        /// <param name="fileNamePattern"></param>
        private void WriteSctEseFiles(Airport airport, String fileNamePattern)
        {
            if (airport == null) throw new ArgumentNullException("airport");
            if (String.IsNullOrEmpty(fileNamePattern)) throw new ArgumentNullException("fileNamePattern");

            const string sct = "sct";
            const string ese = "ese";
            const string sid = "sids";
            const string star = "stars";
            const string transition = "transitions";
            const string appendix = "txt";
            AirportsGroup airportGroup = AirportsGroup.FindInGroups(airport.Name, this.Settings.AirportGroups);
            string groupName = (airportGroup == null) ? "group" : airportGroup.Name;
            int size;

            // SID
            if (!airport.Sids.NullSafeIsEmpty()) {
                string eseSid = airport.ToEse(typeof (Sid), this.Settings);
                string sctSid = airport.ToSct(typeof (Sid), this.Settings);
                string eseSidFileName = this.Settings.DestinationDirectory + ReplacePatternPlaceholders(fileNamePattern, airport.Name, groupName, airport.AiracCycle, sid, ese, appendix);
                string sctSidFileName = this.Settings.DestinationDirectory + ReplacePatternPlaceholders(fileNamePattern, airport.Name, groupName, airport.AiracCycle, sid, sct, appendix);

                if (this.Settings.GenerateEse) {
                    size = this.WriteContentToTexfile(eseSidFileName, eseSid);
                    this.WriteFileStatusMessage(eseSidFileName, size);
                }
                if (this.Settings.GenerateSct) {
                    size = this.WriteContentToTexfile(sctSidFileName, sctSid);
                    this.WriteFileStatusMessage(sctSidFileName, size);
                }
            }

            // STAR
            if (!airport.Stars.NullSafeIsEmpty()) {
                string eseStar = airport.ToEse(typeof (Star), this.Settings);
                string sctStar = airport.ToSct(typeof (Star), this.Settings);
                string eseStarFileName = this.Settings.DestinationDirectory + ReplacePatternPlaceholders(fileNamePattern, airport.Name, groupName, airport.AiracCycle, star, ese, appendix);
                string sctStarFileName = this.Settings.DestinationDirectory + ReplacePatternPlaceholders(fileNamePattern, airport.Name, groupName, airport.AiracCycle, star, sct, appendix);

                if (this.Settings.GenerateEse) {
                    size = this.WriteContentToTexfile(eseStarFileName, eseStar);
                    this.WriteFileStatusMessage(eseStarFileName, size);
                }
                if (this.Settings.GenerateSct) {
                    size = this.WriteContentToTexfile(sctStarFileName, sctStar);
                    this.WriteFileStatusMessage(sctStarFileName, size);
                }
            }

            // Transitions
            if (!airport.Transitions.NullSafeIsEmpty()) {
                string eseTransition = airport.ToEse(typeof (Transition), this.Settings);
                string sctTransition = airport.ToSct(typeof (Transition), this.Settings);
                string eseTransitionFileName = this.Settings.DestinationDirectory + ReplacePatternPlaceholders(fileNamePattern, airport.Name, groupName, airport.AiracCycle, transition, ese, appendix);
                string sctTransitionFileName = this.Settings.DestinationDirectory + ReplacePatternPlaceholders(fileNamePattern, airport.Name, groupName, airport.AiracCycle, transition, sct, appendix);

                if (this.Settings.GenerateEse) {
                    size = this.WriteContentToTexfile(eseTransitionFileName, eseTransition);
                    this.WriteFileStatusMessage(eseTransitionFileName, size);
                }
                if (this.Settings.GenerateSct) {
                    size = this.WriteContentToTexfile(sctTransitionFileName, sctTransition);
                    this.WriteFileStatusMessage(sctTransitionFileName, size);
                }
            }
        }

        /// <summary>
        /// Write to KML file
        /// </summary>
        /// <param name="airport"></param>
        /// <param name="fileNamePattern"></param>
        private void WriteKmlFile(Airport airport, String fileNamePattern)
        {
            if (!this.Settings.GenerateKml) return;
            if (airport == null) throw new ArgumentNullException("airport");
            if (String.IsNullOrEmpty(fileNamePattern)) throw new ArgumentNullException("fileNamePattern");

            AirportsGroup airportGroup = AirportsGroup.FindInGroups(airport.Name, this.Settings.AirportGroups);
            string groupName = (airportGroup == null) ? "group" : airportGroup.Name;
            string kmlFileName = this.Settings.DestinationDirectory + ReplacePatternPlaceholders(fileNamePattern, airport.Name, groupName, airport.AiracCycle, "all", "geodata", "kml");

            string kml = airport.ToKml(this.Settings);
            int size = this.WriteContentToTexfile(kmlFileName, kml);
            this.WriteFileStatusMessage(kmlFileName, size);
        }

        /// <summary>
        /// Status message about the result of writing the file
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="size"></param>
        private void WriteFileStatusMessage(string fileName, int size)
        {
            if (String.IsNullOrEmpty(fileName)) throw new ArgumentNullException("fileName");
            if (size > 0)
                this.WriteStatusMsg(String.Format(Resources.GenerateFileWritten, fileName), ConsoleAndStatus.Severity.Info);
            else if (size == 0)
                this.WriteStatusMsg(String.Format(Resources.GenerateFileEmpty, fileName), ConsoleAndStatus.Severity.Warning);
            else if (size < 0)
                this.WriteStatusMsg(String.Format(Resources.GenerateFileFailed, fileName), ConsoleAndStatus.Severity.Error);
        }

        /// <summary>
        /// Write the file.
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="content"></param>
        /// <param name="trim"></param>
        /// <param name="skipEmpty"></param>
        /// <returns></returns>
        private int WriteContentToTexfile(string fileName, string content, bool trim = true, bool skipEmpty = true)
        {
            if (String.IsNullOrEmpty(fileName)) return -1;
            int size;
            content = content.Replace("\n", "\r\n");
            if (trim) content = content.Trim();
            if (skipEmpty && String.IsNullOrEmpty(content)) {
                File.Delete(fileName);
                return 0;
            }
            try {
                using (StreamWriter outfile = new StreamWriter(fileName)) {
                    outfile.Write(content);
                    outfile.Close();
                    size = content.Length;
                }
            }
            catch {
                size = -1;
            }
            return size;
        }

        /// <summary>
        /// Property changed.
        /// </summary>
        /// <param name="propertyName"></param>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Comment out an ESE or SCT text block
        /// </summary>
        /// <param name="textBlock"></param>
        /// <param name="comment"></param>
        /// <param name="replaceComments"></param>
        /// <returns></returns>
        public static string CommentOutEseOrSctLines(string textBlock, string comment = Generator.SctEseOutputCommentSpace, bool replaceComments = true)
        {
            if (String.IsNullOrEmpty(textBlock)) return textBlock;
            List<string> lines = new List<string>(textBlock.Split('\n'));
            string newCommentNoSpace = comment.Trim();
            StringBuilder sb = new StringBuilder();
            foreach (string line in lines) {
                if (String.IsNullOrEmpty(line)) continue;
                if (line.StartsWith(";")) {
                    string cLine = line;
                    if (replaceComments && !cLine.StartsWith(comment)) {
                        // long comments need to be checked first
                        if (line.StartsWith(Generator.SctEseOutputCommentExtended))
                            cLine = line.Replace(Generator.SctEseOutputCommentExtended, newCommentNoSpace);
                        else if (line.StartsWith(Generator.SctEseOutputComment))
                            cLine = line.Replace(Generator.SctEseOutputComment, newCommentNoSpace);
                    }
                    sb.Append(cLine);
                }
                else {
                    sb.Append(Generator.SctEseOutputCommentExtendedSpace);
                    sb.Append(line);
                }
                sb.Append('\n');
            }
            return sb.ToString();
        }

        /// <summary>
        /// Valid file name from pattern.
        /// </summary>
        /// <param name="pattern"></param>
        /// <param name="airportIcao"></param>
        /// <param name="group"></param>
        /// <param name="airportAirac"></param>
        /// <param name="sidStarTrans"></param>
        /// <param name="fileType"></param>
        /// <param name="appendix"></param>
        /// <returns></returns>
        public static string ReplacePatternPlaceholders(string pattern, string airportIcao, string group, int airportAirac, string sidStarTrans, string fileType, string appendix)
        {
            String fn = pattern;
            if (fileType != null) fn = fn.Replace(PlaceholderFiletype, fileType);
            if (airportAirac >= 0) fn = fn.Replace(PlaceholderAirac, airportAirac.ToString(CultureInfo.InvariantCulture));
            if (airportIcao != null) fn = fn.Replace(PlaceholderAirportIcao, airportIcao);
            if (group != null) fn = fn.Replace(PlaceholderGroup, group);
            if (appendix != null) fn = fn.Replace(PlaceholderAppendix, appendix);
            if (sidStarTrans != null) fn = fn.Replace(PlaceholderType, sidStarTrans);
            return fn;
        }

        /// <summary>
        /// Place by given placeholder / value pairs
        /// </summary>
        /// <param name="pattern"></param>
        /// <param name="placeholderValuePairs"></param>
        /// <returns></returns>
        /// <seealso cref="ReplacePatternPlaceholders(string,string,string,int,string,string,string)"/>
        public static string ReplacePatternPlaceholders(string pattern, Dictionary<string, string> placeholderValuePairs)
        {
            if (String.IsNullOrEmpty(pattern)) return pattern;
            string fn = pattern;
            foreach (KeyValuePair<string, string> placeholferValuePair in placeholderValuePairs) {
                if (!Placeholders.Contains(placeholferValuePair.Key)) throw new ArgumentException("Invalid key " + placeholferValuePair.Key);
                fn = fn.Replace(placeholferValuePair.Key, placeholferValuePair.Value);
            }
            return fn;
        }
    }
}